/**
 * Copyright (C) 2014 Open Ray Message
 * <p>
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * <p>
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.dcpai.datafactory.network.httpservice;

import android.content.Context;
import android.util.Base64;
import android.util.Log;

import com.google.gson.Gson;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.zip.GZIPOutputStream;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;


/**
 * Network interface to the TextSecure server API.
 *
 * @author Moxie Marlinspike
 */
public class PushServiceSocket {
    private static final String TAG = "PushServiceSocket";
    private static final String CREATE_ACCOUNT_PATH = "/accounts/create/%s";
    private static final String CREATE_ACCOUNT_VERIFY_PATH = "/accounts/create/%s/%s";
    private static final String PREKEY_METADATA_PATH_V2 = "/v2/keys/";
    private static final String MULTI_PROTOCOL_PREKEY_PATH = "/v2/keys/%s";
    private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens";
    private static final String MESSAGE_PATH = "/v1/messages/%s";
    private static final String ATTACHMENT_PATH = "/v1/attachments/%s";
    private static final String VERSION_PATH = "/v1/system/check";
    private static final String UPLOAD_CONTACTS_INFO_PATH = "/v1/accounts/code/saveorupdate/%s";
    private static final String GET_CONTACTS_INFO_PATH = "/v1/accounts/code/get/%s";
    private static final String GET_ACCOUNT_PATH = "/accounts/get";
    private static final String GET_ACCOUNT_BY_ID_PATH = "/accounts/getdetail/%s";
    private static final String UPDATE_ACCOUNT_PATH = "/accounts/update";
    private static final String VERIFICATION_CODE_PATH_V2 = "/v2/accounts/reg/code/send/%s";
    private static final String GROUP_MESSAGE_PATH = "/v1/groups/send/%s";
    private static final String GROUP_OPERATE_PATH = "/v1/groups/operate";
    private static final String FEED_BACK_SUGGESTION_PATH = "/v1/userresponse/upload";
    private static final String SYNC_GROUP_INFO_PATH = "/v1/groups/queryinfo";
    private static final String SYNC_SERVER_TIMESTAMP_PATH = "/v1/time/receive";
    private static final String IS_ACCOUNT_VALID_PATH = "/v1/accounts/alivecheck/%s/%s";
    private static final String IS_ACCOUNT_VALID_PATH_BODY = "/accounts/alivecheck/%s";
    private static final String ADD_GROUP_MESSAGE_ACK = "/v1/groups/push/subscibe";
    private static final String ADD_TO_BLACKLIST = "/v1/blacklist/add/%s";
    private static final String REMOVE_FROM_BLACKLIST = "/v1/blacklist/del/%s";
    private static final String OBTAIN_BLACKLIST = "/v1/blacklist/get";
    private static final String UPDATE_GROUP_AUTH_STATUS = "/v1/groups/groupopen";
    private static final String GET_ACCOUNT_INFO_BY_NUMBER = "/accounts/info";
    private static final String GET_FRIENDS_LIST = "/v1/friend/get/all";
    private static final String FRIEND_APLLY_PATH = "/v1/friend/add";
    private static final String GET_GROUP_LIST = "/v1/groups/get/all/%s";
    private static final String GET_GROUP_CHANGE_TIME = "/v1/groups/get/changetime";
    private static final String GET_ENTERPRISE_INFO = "/accounts/entinfo";
    private static final String UPLOAD_DEVICE_INFO = "/accounts/getServer";
    private static final String ENTERPRISE_LIST = "/enterprise/info";
    private static final String ENTERPRISE = "/enterprise/%s";
    private static final String SECTOR = "/enterprise/sector/%s/%s";
    private static final String STAFF = "/enterprise/staffInfo/%s/%s";
    private static final String MY_SECTOR = "/enterprise/sector";

    private final Context context;
    private final String serviceUrl;
    private final TrustManager[] trustManagers;
    private final String topic;
    private final long accountId;

    public enum FileEnum {
        TEMP(1), AVATAR(2);

        private int type;

        FileEnum(int type) {
            this.type = type;
        }

        public int getType() {
            return type;
        }
    }

    public PushServiceSocket(Context context, String serviceUrl, TrustStore trustStore,
                             long accountId, String topic) {
        this.context = context.getApplicationContext();
        this.serviceUrl = serviceUrl;
        this.trustManagers = mTrustAllCerts;
        this.accountId = accountId;
        this.topic = topic;
    }

    /**
     * 注册
     *
     * @return 账户信息
     * @throws IOException
     */
    public BasicAccountInfo registerAccount(String localNumber) throws IOException {
        List<Integer> secureProtocolVersionList = new ArrayList<>();
        secureProtocolVersionList.add(1);
        ProtocolList protocolList = new ProtocolList(secureProtocolVersionList);
        String response = makeRequest(String.format(CREATE_ACCOUNT_PATH, localNumber), "PUT", new Gson().toJson(protocolList));
        return new Gson().fromJson(response, BasicAccountInfo.class);
    }

    public BasicAccountInfo registerAccountWithVerifiycationCode(String localNumber, String verificationCode) throws IOException {
        List<Integer> secureProtocolVersionList = new ArrayList<>();
        secureProtocolVersionList.add(1);
        ProtocolList protocolList = new ProtocolList(secureProtocolVersionList);
        String response = makeRequest(String.format(CREATE_ACCOUNT_VERIFY_PATH, localNumber, verificationCode), "PUT", new Gson().toJson(protocolList));
        return new Gson().fromJson(response, BasicAccountInfo.class);
    }


    public long requestServerTimeStamp() {
        try {
            String response = makeRequest(SYNC_SERVER_TIMESTAMP_PATH, "GET", null);
            return Long.parseLong(response);
        } catch (IOException | NumberFormatException ioe) {
            Log.w("PushServiceSocket", ioe);
        }
        return 0;
    }

    private byte[] getBytes(char[] chars) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        PrintWriter writer = new PrintWriter(outputStream);
        writer.print(chars);
        writer.close();
        return outputStream.toByteArray();
    }

    /**
     * check account valid.
     * @param accountName
     * @param password
     * @return return true if login on the other device, otherwise return false.
     * @throws IOException network exception.
     */
    public boolean isAccountValid(long accountName, char[] password) throws IOException {
        byte[] passwordByte;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            messageDigest.update(getBytes(password));
            passwordByte = messageDigest.digest();
        } catch (NoSuchAlgorithmException e) {
            passwordByte = new byte[0];
        }
        String response = makeRequestBinary(String.format(IS_ACCOUNT_VALID_PATH_BODY, accountName), "PUT", passwordByte);
        return "true".equals(response);
    }

    public boolean isAccountValid(String localNumber, String clientId) throws IOException {
        String response = makeRequest(String.format(IS_ACCOUNT_VALID_PATH, localNumber, clientId), "GET", null);
        return "true".equals(response);
    }

    private String mergeArraryStringToString(List<String> groupIDs) {
        StringBuffer mergeResult = new StringBuffer();
        for (String groupID : groupIDs) {
            mergeResult.append(groupID + "-");
        }
        return mergeResult.toString();
    }

    public void ackAddGroup(List<String> groupIDs) throws IOException {
        makeRequest(ADD_GROUP_MESSAGE_ACK, "PUT", new Gson().toJson(groupIDs));
    }

    public StatusModel requestVerificationCodeV2(String phoneNumber) {
        try {
            String response = makeRequest(String.format(VERIFICATION_CODE_PATH_V2, phoneNumber), "GET",
                    null);
            return new Gson().fromJson(response, StatusModel.class);
        } catch (IOException ioe) {
            Log.w("PushServiceSocket", ioe);
        }
        return null;
    }

    public void syncGroupInfo() throws IOException {
        makeRequest(SYNC_GROUP_INFO_PATH, "PUT", "");
    }

    /**
     * send group control message
     *
     * @param builder MessageSignal
     * @return byte[] MessageSignal
     * @throws IOException
     */
    public byte[] sendGroupControlMessage(byte[] builder) throws IOException {
        return makeRequestBinaryReturnBytes(GROUP_OPERATE_PATH, "PUT", builder);
    }

    /**
     * get group single or list(-1)
     *
     * @return byte[]
     * @throws IOException
     */
    public byte[] getGroupList(long groupId) throws IOException {
        return makeRequestBinaryReturnBytes(String.format(GET_GROUP_LIST, groupId), "GET", null);
    }

    public void sendFriendMessage(byte[] builder) throws IOException {
        makeRequestBinary(FRIEND_APLLY_PATH, "PUT", builder);
    }

    public void sendGroupMessage(long groupId, byte[] message)
            throws IOException {
    }

    public void sendMessage(long remoteId, byte[] message)
            throws IOException {

    }


    public File retrieveAttachment(String relay, long attachmentId) throws IOException {
        String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));


        String response = makeRequest(path, "GET", null);
        AttachmentDescriptor descriptor = new Gson().fromJson(response, AttachmentDescriptor.class);

        Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());

        File attachment = File.createTempFile("attachment", ".tmp", context.getFilesDir());
        attachment.deleteOnExit();

        downloadExternalFile(descriptor.getLocation(), attachment);

        return attachment;
    }

    public File retrieveAttachment(Context context, String name, String relay, long attachmentId) throws IOException {
        String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));

        String response = makeRequest(path, "GET", null);
        AttachmentDescriptor descriptor = new Gson().fromJson(response, AttachmentDescriptor.class);
        Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());
        File file = new File(context.getFilesDir(), name);
        if (!file.exists()) {
            file.createNewFile();
        }
        downloadExternalFile(descriptor.getLocation(), file);
        return file;
    }

    private void downloadExternalFile(String url, File localDestination)
            throws IOException {
        URL downloadUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
//    connection.setRequestProperty("Content-Type", "application/octet-stream");
        //for ali yun OSS service
        connection.setRequestProperty("Content-Type", "");
        connection.setRequestMethod("GET");
        connection.setDoInput(true);

        try {
            if (connection.getResponseCode() != 200) {
                throw new IOException("Bad response: " + connection.getResponseCode());
            }

            OutputStream output = new FileOutputStream(localDestination);
            InputStream input = connection.getInputStream();
            byte[] buffer = new byte[4096];
            int read;

            while ((read = input.read(buffer)) != -1) {
                output.write(buffer, 0, read);
            }

            output.close();
            Log.w("PushServiceSocket", "Downloaded: " + url + " to: " + localDestination.getAbsolutePath());
        } catch (ConcurrentModificationException cme) {
            Log.w("PushServiceSocket", cme);
            throw new IOException();
        } finally {
            connection.disconnect();
        }
    }

    /**
     * add to blacklist
     *
     * @param number
     * @return ReturnInfo
     * @throws IOException
     */
    public ReturnInfo addToBlacklist(String number) throws IOException {
        String response = makeRequest(String.format(ADD_TO_BLACKLIST, number), "PUT", null);
        return new Gson().fromJson(response, ReturnInfo.class);
    }

    /**
     * remove from blacklist
     *
     * @param number
     * @return ReturnInfo
     * @throws IOException
     */
    public ReturnInfo removeFromBlacklist(String number) throws IOException {
        String response = makeRequest(String.format(REMOVE_FROM_BLACKLIST, number), "DELETE", null);
        return new Gson().fromJson(response, ReturnInfo.class);
    }

    /**
     * get blacklist
     *
     * @return
     * @throws IOException
     */
    public BlackList getBlackList() throws IOException {
        String response = makeRequest(OBTAIN_BLACKLIST, "GET", null);
        return new Gson().fromJson(response, BlackList.class);
    }

    /**
     * 设置群组认证状态
     *
     * @param isAuthGroup true表示打开，false表示关闭
     * @throws IOException
     */
    public GroupOpenStatus updateGroupAuthStatus(boolean isAuthGroup) throws IOException {
        String response = makeRequest(UPDATE_GROUP_AUTH_STATUS + "/set?isopen=" + isAuthGroup, "GET", null);
        return new Gson().fromJson(response, GroupOpenStatus.class);
    }

    /**
     * 获取群组认证状态
     *
     * @throws IOException
     */
//    public boolean getGroupAuthStatus() throws IOException {
//        String response = makeRequest(GET_GROUP_AUTH_STATUS, "GET", null);
//        return Boolean.valueOf(response);
//    }
    public long getGroupUpdateTime(long groupId) throws IOException {
        String response = makeRequest(GET_GROUP_CHANGE_TIME + "/" + groupId, "GET", null);
        return Long.parseLong(response);
    }

    private void uploadExternalFile(String method, String url, byte[] data)
            throws IOException {
        URL uploadUrl = new URL(url);
        //Modified by wei.he for not using https connection in our server
//    HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection();
        HttpURLConnection connection = (HttpURLConnection) uploadUrl.openConnection();
        connection.setDoOutput(true);
        connection.setRequestMethod(method);
//    connection.setRequestProperty("Content-Type", "application/octet-stream");
        //for ali yun OSS service
        connection.setRequestProperty("Content-Type", "");
        connection.connect();

        try {
            OutputStream out = connection.getOutputStream();
            out.write(data);
            out.close();

            //Modified by wei.he for aliyun returns 201 swift returns 200
            if (!String.valueOf(connection.getResponseCode()).startsWith("20")) {
                throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
            }
        } finally {
            connection.disconnect();
        }
    }

    private String makeRequest(String urlFragment, String method, String body)
            throws IOException {
        HttpURLConnection connection = makeRequestWithContentType(urlFragment, method, body == null ?
                null : body.getBytes(), "application/json");
        String response = readFully(connection.getInputStream());

        connection.disconnect();

        return response;
    }

    private String makeRequestBinary(String urlFragment, String method, byte[] body) throws IOException {
        HttpURLConnection connection = makeRequestWithContentType(urlFragment, method, body, "application/octet-stream");
        String response = readFully(connection.getInputStream());
        connection.disconnect();
        return response;
    }

    private byte[] makeRequestBinaryReturnBytes(String urlFragment, String method, byte[] body)
            throws IOException {
        HttpURLConnection connection = makeRequestWithContentType(urlFragment, method, body,
                "application/octet-stream");
        byte[] response = readFullyReturnBytes(connection.getInputStream());

        connection.disconnect();

        return response;
    }

    private HttpURLConnection makeRequestWithContentType(String urlFragment, String method, byte[] body, String contentType) throws IOException {
        HttpURLConnection connection = getConnection(urlFragment, method, contentType);

        if (body != null) {
            connection.setDoOutput(true);
            if (body.length > 256 && "PUT".equals(method)) {
                connection.setRequestProperty("Content-Encoding", "gzip");
            }
        }
        connection.connect();
        if (body != null) {
            OutputStream out = connection.getOutputStream();
            if (body.length > 256 && "PUT".equals(method)) {
                ByteArrayOutputStream originalContent = new ByteArrayOutputStream();
                originalContent.write(body);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
                originalContent.writeTo(gzipOut);
                gzipOut.finish();
                out.write(baos.toByteArray());
            } else {
                out.write(body);
            }
            out.close();
        }

        if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
            throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
        }

        return connection;
    }

    private HttpURLConnection getConnection(String urlFragment, String method, String contentType)
            throws
            IOException {
        try {
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, trustManagers, null);

            URL url = new URL(String.format("%s%s", serviceUrl, urlFragment));
            Log.d("PushServiceSocket", "Opening URL: " + url + "    method:" + method + "   contentType:" + contentType);

            HttpURLConnection connection = (HttpURLConnection) url.openConnection();

            if (connection instanceof HttpsURLConnection) {
                ((HttpsURLConnection) connection).setSSLSocketFactory(context.getSocketFactory());
                //Comment by Wei.He 2014-08-21
//        ((HttpsURLConnection)connection).setHostnameVerifier(new StrictHostnameVerifier());
                ((HttpsURLConnection) connection).setHostnameVerifier(DO_NOT_VERIFY);
            }

            connection.setConnectTimeout(7000);
            connection.setReadTimeout(7000);
            connection.setRequestMethod(method);
            connection.setRequestProperty("Content-Type", contentType);

            if (topic != null) {
                connection.setRequestProperty("Authorization", getAuthorizationHeader());
            }

            return connection;
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        } catch (KeyManagementException e) {
            throw new AssertionError(e);
        } catch (MalformedURLException e) {
            throw new AssertionError(e);
        }
    }

    // Added by Wei.He for do not check for always verify the host - dont check for certificate
    private static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
        public boolean verify(String hostname, javax.net.ssl.SSLSession session) {
            return true;
        }
    };

    private String getAuthorizationHeader() {
        try {
            MessageDigest mdInst = MessageDigest.getInstance("MD5");
            if (topic != null)
                mdInst.update(topic.getBytes("UTF-8"));
            byte[] digest = mdInst.digest();
            return "Basic ";
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError(e);
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        }
    }

    // Adde by Wei.He 2014-08-21
    // Create a trust manager that does not validate certificate chains
    // Android 采用X509的证书信息机制
    TrustManager[] mTrustAllCerts = new TrustManager[]{new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[]{};
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }
    }
    };

    public String getSecret(int size) {
        try {
            byte[] secret = new byte[size];
            SecureRandom.getInstance("SHA1PRNG").nextBytes(secret);
            return Base64.encodeToString(secret, Base64.DEFAULT);
        } catch (NoSuchAlgorithmException nsae) {
            throw new AssertionError(nsae);
        }
    }

    public String readFully(File file) throws IOException {
        return readFully(new FileInputStream(file));
    }

    public String readFully(InputStream in) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int read;

        while ((read = in.read(buffer)) != -1) {
            bout.write(buffer, 0, read);
        }
        in.close();
        return new String(bout.toByteArray());
    }

    public byte[] readFullyReturnBytes(InputStream in) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int read;

        while ((read = in.read(buffer)) != -1) {
            bout.write(buffer, 0, read);
        }

        in.close();

        return bout.toByteArray();
    }

    private static class AttachmentDescriptor {
        private long id;
        private String location;

        public long getId() {
            return id;
        }

        public String getLocation() {
            return location;
        }
    }

    public interface TrustStore {
        public InputStream getKeyStoreInputStream();

        public String getKeyStorePassword();
    }

    public static class StatusModel {
        //@SerializedName("code")
        private int code;

        public int geCode() {
            return code;
        }

        public void setCode(int status) {
            this.code = status;
        }
    }

    public static class ReturnInfo {
        private Boolean isSuccess;
        private String message;
        private String name;

        public Boolean getIsSuccess() {
            return isSuccess;
        }

        public void setIsSuccess(Boolean isSuccess) {
            this.isSuccess = isSuccess;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    public static class BlackList {
        private String blacklist;

        public String getBlacklist() {
            return blacklist;
        }
    }

    public static class GroupOpenStatus {
        //success,fail
        public String isSuccess;
        //true,false
        public Boolean currentStatus;

        public String getIsSuccess() {
            return isSuccess;
        }

        public void setIsSuccess(String isSuccess) {
            this.isSuccess = isSuccess;
        }

        public Boolean getCurrentStatus() {
            return currentStatus;
        }

        public void setCurrentStatus(Boolean currentStatus) {
            this.currentStatus = currentStatus;
        }
    }

    public static class BasicAccountInfo {
        private Long accountId;

        private String topic;
        private String mqttpassword;


        public String getMqttpassword() {
            return mqttpassword;
        }

        public Long getAccountId() {
            return accountId;
        }

        public void setAccountId(Long accountId) {
            this.accountId = accountId;
        }

        public String getTopic() {
            return topic;
        }

        public void setTopic(String topic) {
            this.topic = topic;
        }

        public void setMqttpassword(String mqttpassword) {
            this.mqttpassword = mqttpassword;
        }

        @Override
        public String toString() {
            return new String("TOPIC = " + topic + " , registerid = " + accountId + " , mqttpassword = " + mqttpassword);
        }
    }

    public static class ProtocolList {
        private List<Integer> protoList;

        public ProtocolList(List<Integer> protoList) {
            this.protoList = protoList;
        }

        public List<Integer> getProtoList() {
            return protoList;
        }

        public void setProtoList(List<Integer> protoList) {
            this.protoList = protoList;
        }
    }

}
